# Windows dialog .RC file parser, by Adam Walker.

# This module was adapted from the spambayes project, and is Copyright
# 2003/2004 The Python Software Foundation and is covered by the Python
# Software Foundation license.
"""
This is a parser for Windows .rc files, which are text files which define
dialogs and other Windows UI resources.
"""
__author__="Adam Walker"
__version__="0.11"

import sys, os, shlex, stat
import pprint
import win32con
import commctrl

_controlMap = {"DEFPUSHBUTTON":0x80,
               "PUSHBUTTON":0x80,
               "Button":0x80,
               "GROUPBOX":0x80,
               "Static":0x82,
               "CTEXT":0x82,
               "RTEXT":0x82,
               "LTEXT":0x82,
               "LISTBOX":0x83,
               "SCROLLBAR":0x84,
               "COMBOBOX":0x85,
               "EDITTEXT":0x81,
               "ICON":0x82,
               "RICHEDIT":"RichEdit20A"
               }

# These are "default styles" for certain controls - ie, Visual Studio assumes
# the styles will be applied, and emits a "NOT {STYLE_NAME}" if it is to be
# disabled.  These defaults have been determined by experimentation, so may
# not be completely accurate (most notably, some styles and/or control-types
# may be missing.
_addDefaults = {"EDITTEXT":win32con.WS_BORDER | win32con.WS_TABSTOP,
                "GROUPBOX":win32con.BS_GROUPBOX,
                "LTEXT":win32con.SS_LEFT,
                "DEFPUSHBUTTON":win32con.BS_DEFPUSHBUTTON | win32con.WS_TABSTOP,
                "PUSHBUTTON": win32con.WS_TABSTOP,
                "CTEXT":win32con.SS_CENTER,
                "RTEXT":win32con.SS_RIGHT,
                "ICON":win32con.SS_ICON,
                "LISTBOX":win32con.LBS_NOTIFY,
                }

defaultControlStyle = win32con.WS_CHILD | win32con.WS_VISIBLE
defaultControlStyleEx = 0

class DialogDef:
    name = ""
    id = 0
    style = 0
    styleEx = None
    caption = ""
    font = "MS Sans Serif"
    fontSize = 8
    x = 0
    y = 0
    w = 0
    h = 0
    template = None
    def __init__(self, n, i):
        self.name = n
        self.id = i
        self.styles = []
        self.stylesEx = []
        self.controls = []
        #print "dialog def for ",self.name, self.id
    def createDialogTemplate(self):
        t = None
        self.template = [[self.caption,
                          (self.x,self.y,self.w,self.h),
                          self.style, self.styleEx,
                          (self.fontSize, self.font)]
        ]
        # Add the controls
        for control in self.controls:
            self.template.append(control.createDialogTemplate())
        return self.template

class ControlDef:
    id = ""
    controlType = ""
    subType = ""
    idNum = 0
    style = defaultControlStyle
    styleEx = defaultControlStyleEx
    label = ""
    x = 0
    y = 0
    w = 0
    h = 0
    def __init__(self):
        self.styles = []
        self.stylesEx = []
    def toString(self):
        s = "<Control id:"+self.id+" controlType:"+self.controlType+" subType:"+self.subType\
            +" idNum:"+str(self.idNum)+" style:"+str(self.style)+" styles:"+str(self.styles)+" label:"+self.label\
            +" x:"+str(self.x)+" y:"+str(self.y)+" w:"+str(self.w)+" h:"+str(self.h)+">"
        return s
    def createDialogTemplate(self):
        ct = self.controlType
        if "CONTROL"==ct:
            ct = self.subType
        if ct in _controlMap:
            ct = _controlMap[ct]
        t = [ct, self.label, self.idNum, (self.x, self.y, self.w, self.h), self.style, self.styleEx]
        #print t
        return t

class StringDef:
    def __init__(self, id, idNum, value):
        self.id = id
        self.idNum = idNum
        self.value = value

    def __repr__(self):
        return "StringDef(%r, %r, %r)" % (self.id, self.idNum, self.value)

class RCParser:
    next_id = 1001
    dialogs = {}
    _dialogs = {}
    debugEnabled = False
    token = ""

    def __init__(self):
        self.ungot = False
        self.ids = {"IDC_STATIC": -1}
        self.names = {-1:"IDC_STATIC"}
        self.bitmaps = {}
        self.stringTable = {}
        self.icons = {}

    def debug(self, *args):
        if self.debugEnabled:
            print(args)

    def getToken(self):
        if self.ungot:
            self.ungot = False
            self.debug("getToken returns (ungot):", self.token)
            return self.token
        self.token = self.lex.get_token()
        self.debug("getToken returns:", self.token)
        if self.token=="":
            self.token = None
        return self.token

    def ungetToken(self):
        self.ungot = True

    def getCheckToken(self, expected):
        tok = self.getToken()
        assert tok == expected, "Expected token '%s', but got token '%s'!" % (expected, tok)
        return tok

    def getCommaToken(self):
        return self.getCheckToken(",")

    # Return the *current* token as a number, only consuming a token
    # if it is the negative-sign.
    def currentNumberToken(self):
        mult = 1
        if self.token=='-':
            mult = -1
            self.getToken()
        return int(self.token) * mult

    # Return the *current* token as a string literal (ie, self.token will be a
    # quote.  consumes all tokens until the end of the string
    def currentQuotedString(self):
        # Handle quoted strings - pity shlex doesn't handle it.
        assert self.token.startswith('"'), self.token
        bits = [self.token]
        while 1:
            tok = self.getToken()
            if not tok.startswith('"'):
                self.ungetToken()
                break
            bits.append(tok)
        sval = "".join(bits)[1:-1] # Remove end quotes.            
        # Fixup quotes in the body, and all (some?) quoted characters back
        # to their raw value.
        for i, o in ('""', '"'), ("\\r", "\r"), ("\\n", "\n"), ("\\t", "\t"):
            sval = sval.replace(i, o)
        return sval
        
    def load(self, rcstream):
        """
        RCParser.loadDialogs(rcFileName) -> None
        Load the dialog information into the parser. Dialog Definations can then be accessed
        using the "dialogs" dictionary member (name->DialogDef). The "ids" member contains the dictionary of id->name.
        The "names" member contains the dictionary of name->id
        """
        self.open(rcstream)
        self.getToken()
        while self.token!=None:
            self.parse()
            self.getToken()

    def open(self, rcstream):
        self.lex = shlex.shlex(rcstream)
        self.lex.commenters = "//#"

    def parseH(self, file):
        lex = shlex.shlex(file)
        lex.commenters = "//"
        token = " "
        while token is not None:
            token = lex.get_token()
            if token == "" or token is None:
                token = None
            else:
                if token=='define':
                    n = lex.get_token()
                    i = int(lex.get_token())
                    self.ids[n] = i
                    if i in self.names:
                        # Dupe ID really isn't a problem - most consumers
                        # want to go from name->id, and this is OK.
                        # It means you can't go from id->name though.
                        pass
                        # ignore AppStudio special ones
                        #if not n.startswith("_APS_"):
                        #    print "Duplicate id",i,"for",n,"is", self.names[i]
                    else:
                        self.names[i] = n
                    if self.next_id<=i:
                        self.next_id = i+1

    def parse(self):
        noid_parsers = {
            "STRINGTABLE":      self.parse_stringtable,
        }

        id_parsers = {
            "DIALOG" :          self.parse_dialog,
            "DIALOGEX":         self.parse_dialog,
#            "TEXTINCLUDE":      self.parse_textinclude,
            "BITMAP":           self.parse_bitmap,
            "ICON":             self.parse_icon,
        }
        deep = 0
        base_token = self.token
        rp = noid_parsers.get(base_token)
        if rp is not None:
            rp()
        else:
            # Not something we parse that isn't prefixed by an ID
            # See if it is an ID prefixed item - if it is, our token
            # is the resource ID.
            resource_id = self.token
            self.getToken()
            if self.token is None:
                return

            if "BEGIN" == self.token:
                # A 'BEGIN' for a structure we don't understand - skip to the
                # matching 'END'
                deep = 1
                while deep!=0 and self.token is not None:
                    self.getToken()
                    self.debug("Zooming over", self.token)
                    if "BEGIN" == self.token:
                        deep += 1
                    elif "END" == self.token:
                        deep -= 1
            else:
                rp = id_parsers.get(self.token)
                if rp is not None:
                    self.debug("Dispatching '%s'" % (self.token,))
                    rp(resource_id)
                else:
                    # We don't know what the resource type is, but we
                    # have already consumed the next, which can cause problems,
                    # so push it back.
                    self.debug("Skipping top-level '%s'" % base_token)
                    self.ungetToken()

    def addId(self, id_name):
        if id_name in self.ids:
            id = self.ids[id_name]
        else:
            # IDOK, IDCANCEL etc are special - if a real resource has this value
            for n in ["IDOK","IDCANCEL","IDYES","IDNO", "IDABORT"]:
                if id_name == n:
                    v = getattr(win32con, n)
                    self.ids[n] = v
                    self.names[v] = n
                    return v
            id = self.next_id
            self.next_id += 1
            self.ids[id_name] = id
            self.names[id] = id_name
        return id

    def lang(self):
        while self.token[0:4]=="LANG" or self.token[0:7]=="SUBLANG" or self.token==',':
            self.getToken();

    def parse_textinclude(self, res_id):
        while self.getToken() != "BEGIN":
            pass
        while 1:
            if self.token == "END":
                break
            s = self.getToken()

    def parse_stringtable(self):
        while self.getToken() != "BEGIN":
            pass
        while 1:
            self.getToken()
            if self.token == "END":
                break
            sid = self.token
            self.getToken()
            sd = StringDef(sid, self.addId(sid), self.currentQuotedString())
            self.stringTable[sid] = sd

    def parse_bitmap(self, name):
        return self.parse_bitmap_or_icon(name, self.bitmaps)

    def parse_icon(self, name):
        return self.parse_bitmap_or_icon(name, self.icons)

    def parse_bitmap_or_icon(self, name, dic):
        self.getToken()
        while not self.token.startswith('"'):
            self.getToken()
        bmf = self.token[1:-1] # quotes
        dic[name] = bmf

    def parse_dialog(self, name):
        dlg = DialogDef(name,self.addId(name))
        assert len(dlg.controls)==0
        self._dialogs[name] = dlg
        extras = []
        self.getToken()
        while not self.token.isdigit():
            self.debug("extra", self.token)
            extras.append(self.token)
            self.getToken()
        dlg.x = int(self.token)
        self.getCommaToken()
        self.getToken() # number
        dlg.y = int(self.token)
        self.getCommaToken()
        self.getToken() # number
        dlg.w = int(self.token)
        self.getCommaToken()
        self.getToken() # number
        dlg.h = int(self.token)
        self.getToken()
        while not (self.token==None or self.token=="" or self.token=="END"):
            if self.token=="STYLE":
                self.dialogStyle(dlg)
            elif self.token=="EXSTYLE":
                self.dialogExStyle(dlg)
            elif self.token=="CAPTION":
                self.dialogCaption(dlg)
            elif self.token=="FONT":
                self.dialogFont(dlg)
            elif self.token=="BEGIN":
                self.controls(dlg)
            else:
                break
        self.dialogs[name] = dlg.createDialogTemplate()

    def dialogStyle(self, dlg):
        dlg.style, dlg.styles = self.styles( [], win32con.DS_SETFONT)
    def dialogExStyle(self, dlg):
        self.getToken()
        dlg.styleEx, dlg.stylesEx = self.styles( [], 0)

    def styles(self, defaults, defaultStyle):
        list = defaults
        style = defaultStyle

        if "STYLE"==self.token:
            self.getToken()
        i = 0
        Not = False
        while ((i%2==1 and ("|"==self.token or "NOT"==self.token)) or (i%2==0)) and not self.token==None:
            Not = False;
            if "NOT"==self.token:
                Not = True
                self.getToken()
            i += 1
            if self.token!="|":
                if self.token in win32con.__dict__:
                    value = getattr(win32con,self.token)
                else:
                    if self.token in commctrl.__dict__:
                        value = getattr(commctrl,self.token)
                    else:
                        value = 0
                if Not:
                    list.append("NOT "+self.token)
                    self.debug("styles add Not",self.token, value)
                    style &= ~value
                else:
                    list.append(self.token)
                    self.debug("styles add", self.token, value)
                    style |= value
            self.getToken()
        self.debug("style is ",style)

        return style, list

    def dialogCaption(self, dlg):
        if "CAPTION"==self.token:
            self.getToken()
        self.token = self.token[1:-1]
        self.debug("Caption is:",self.token)
        dlg.caption = self.token
        self.getToken()
    def dialogFont(self, dlg):
        if "FONT"==self.token:
            self.getToken()
        dlg.fontSize = int(self.token)
        self.getCommaToken()
        self.getToken() # Font name
        dlg.font = self.token[1:-1] # it's quoted
        self.getToken()
        while "BEGIN"!=self.token:
            self.getToken()
    def controls(self, dlg):
        if self.token=="BEGIN": self.getToken()
        # All controls look vaguely like:
        # TYPE [text, ] Control_id, l, t, r, b [, style]
        # .rc parser documents all control types as:
        # CHECKBOX, COMBOBOX, CONTROL, CTEXT, DEFPUSHBUTTON, EDITTEXT, GROUPBOX,
        # ICON, LISTBOX, LTEXT, PUSHBUTTON, RADIOBUTTON, RTEXT, SCROLLBAR
        without_text = ["EDITTEXT", "COMBOBOX", "LISTBOX", "SCROLLBAR"]
        while self.token!="END":
            control = ControlDef()
            control.controlType = self.token;
            self.getToken()
            if control.controlType not in without_text:
                if self.token[0:1]=='"':
                    control.label = self.currentQuotedString()
                # Some funny controls, like icons and picture controls use
                # the "window text" as extra resource ID (ie, the ID of the
                # icon itself).  This may be either a literal, or an ID string.
                elif self.token=="-" or self.token.isdigit():
                    control.label = str(self.currentNumberToken())
                else:
                    # An ID - use the numeric equiv.
                    control.label = str(self.addId(self.token))
                self.getCommaToken()
                self.getToken()
            # Control IDs may be "names" or literal ints
            if self.token=="-" or self.token.isdigit():
                control.id = self.currentNumberToken()
                control.idNum = control.id
            else:
                # name of an ID
                control.id = self.token
                control.idNum = self.addId(control.id)
            self.getCommaToken()

            if control.controlType == "CONTROL":
                self.getToken()
                control.subType = self.token[1:-1]
                thisDefaultStyle = defaultControlStyle | \
                                   _addDefaults.get(control.subType, 0)
                # Styles
                self.getCommaToken()
                self.getToken()
                control.style, control.styles = self.styles([], thisDefaultStyle)
            else:
                thisDefaultStyle = defaultControlStyle | \
                                   _addDefaults.get(control.controlType, 0)
                # incase no style is specified.
                control.style = thisDefaultStyle
            # Rect
            control.x = int(self.getToken())
            self.getCommaToken()
            control.y = int(self.getToken())
            self.getCommaToken()
            control.w = int(self.getToken())
            self.getCommaToken()
            self.getToken()
            control.h = int(self.token)
            self.getToken()
            if self.token==",":
                self.getToken()
                control.style, control.styles = self.styles([], thisDefaultStyle)
            if self.token==",":
                self.getToken()
                control.styleEx, control.stylesEx = self.styles([], defaultControlStyleEx)
            #print control.toString()
            dlg.controls.append(control)

def ParseStreams(rc_file, h_file):
    rcp = RCParser()
    if h_file:
        rcp.parseH(h_file)
    try:
        rcp.load(rc_file)
    except:
        lex = getattr(rcp, "lex", None)
        if lex:
            print("ERROR parsing dialogs at line", lex.lineno)
            print("Next 10 tokens are:")
            for i in range(10):
                print(lex.get_token(), end=' ')
            print()
        raise
    return rcp
    
def Parse(rc_name, h_name = None):
    if h_name:
        h_file = open(h_name, "rU")
    else:
        # See if same basename as the .rc
        h_name = rc_name[:-2]+"h"
        try:
            h_file = open(h_name, "rU")
        except IOError:
            # See if MSVC default of 'resource.h' in the same dir.
            h_name = os.path.join(os.path.dirname(rc_name), "resource.h")
            try:
                h_file = open(h_name, "rU")
            except IOError:
                # .h files are optional anyway
                h_file = None
    rc_file = open(rc_name, "rU")
    try:
        return ParseStreams(rc_file, h_file)
    finally:
        if h_file is not None:
            h_file.close()
        rc_file.close()
    return rcp

def GenerateFrozenResource(rc_name, output_name, h_name = None):
    """Converts an .rc windows resource source file into a python source file
       with the same basic public interface as the rest of this module.
       Particularly useful for py2exe or other 'freeze' type solutions,
       where a frozen .py file can be used inplace of a real .rc file.
    """
    rcp = Parse(rc_name, h_name)
    in_stat = os.stat(rc_name)

    out = open(output_name, "wt")
    out.write("#%s\n" % output_name)
    out.write("#This is a generated file. Please edit %s instead.\n" % rc_name)
    out.write("__version__=%r\n" % __version__)
    out.write("_rc_size_=%d\n_rc_mtime_=%d\n" % (in_stat[stat.ST_SIZE], in_stat[stat.ST_MTIME]))

    out.write("class StringDef:\n")
    out.write("\tdef __init__(self, id, idNum, value):\n")
    out.write("\t\tself.id = id\n")
    out.write("\t\tself.idNum = idNum\n")
    out.write("\t\tself.value = value\n")
    out.write("\tdef __repr__(self):\n")
    out.write("\t\treturn \"StringDef(%r, %r, %r)\" % (self.id, self.idNum, self.value)\n")

    out.write("class FakeParser:\n")

    for name in "dialogs", "ids", "names", "bitmaps", "icons", "stringTable":
        out.write("\t%s = \\\n" % (name,))
        pprint.pprint(getattr(rcp, name), out)
        out.write("\n")

    out.write("def Parse(s):\n")
    out.write("\treturn FakeParser()\n")
    out.close()

if __name__=='__main__':
    if len(sys.argv) <= 1:
        print(__doc__)
        print()
        print("See test_win32rcparser.py, and the win32rcparser directory (both")
        print("in the test suite) for an example of this module's usage.")
    else:
        import pprint
        filename = sys.argv[1]
        if "-v" in sys.argv:
            RCParser.debugEnabled = 1
        print("Dumping all resources in '%s'" % filename)
        resources = Parse(filename)
        for id, ddef in resources.dialogs.items():
            print("Dialog %s (%d controls)" % (id, len(ddef)))
            pprint.pprint(ddef)
            print()
        for id, sdef in resources.stringTable.items():
            print("String %s=%r" % (id, sdef.value))
            print()
        for id, sdef in resources.bitmaps.items():
            print("Bitmap %s=%r" % (id, sdef))
            print()
        for id, sdef in resources.icons.items():
            print("Icon %s=%r" % (id, sdef))
            print()
